package se.kayarr.cardplay.gameplay;

import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

import lombok.AccessLevel;
import lombok.Getter;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Undefined;

import se.kayarr.cardplay.backend.Initializer;
import se.kayarr.cardplay.exceptions.UnsetVariableException;
import se.kayarr.cardplay.shared.Util;

public abstract class Card {
	public static final String TYPE_SYMBOL = "?";
	
	@Getter(AccessLevel.PACKAGE) private Scriptable cardScope;
	
	@Getter private int numericId = 0;
	@Getter private String refId = "";
	@Getter private String name = "";
	@Getter private String desc = "";
	
	public void setRefId(String refId) {
		checkMutable();
		this.refId = refId;
	}

	public void setName(String name) {
		checkMutable();
		this.name = name;
	}

	public void setDesc(String desc) {
		checkMutable();
		this.desc = desc;
	}
	
	protected void checkMutable() {
		if(!mutableState) throw new IllegalStateException("Can not modify card after \"setupCardInfo()\" has finished");
	}
	
	protected boolean mutableState = true;
	
	private void resetInfo() {
		refId = "";
		name = "";
		desc = "";
	}
	
	protected Card(int numericId) {
		this.cardScope = null;
		this.numericId = numericId;
	}
	
	public String getDisplayId() {
		return getTypeSymbol() + (numericId > 0 ? String.format("%3s",
					Util.padAtStart(Integer.toString(numericId, 16).toUpperCase(), 3, '0')) : "---");
	};
	
	public String getDisplayName() {
		return "[" + getDisplayId() + " \"" + name + "\"]";
	}
	
	public String getTypeSymbol() {
		return TYPE_SYMBOL;
	}
	
	void loadScript(Path scriptFile) throws IOException, UnsetVariableException {
		resetInfo();
		cardScope = Initializer.newScriptScope();
		cardScope.put("card", cardScope, Context.javaToJS(this, cardScope));
		
		Reader fileReader = Files.newBufferedReader(scriptFile, Charset.defaultCharset());
		Initializer.getScriptContext().evaluateReader(cardScope, fileReader, scriptFile.getFileName().toString(), 1, null);
		invokeRequiredFunction("setupCardData");
		
		checkRequiredVarsSet();
		
		mutableState = false;
	}
	
	protected void checkRequiredVarsSet() throws UnsetVariableException {
		if(name == null || name.length() == 0) throw new UnsetVariableException("name");
		if(desc == null || desc.length() == 0) throw new UnsetVariableException("desc");
		if(refId == null || refId.length() == 0) throw new UnsetVariableException("refId");
	}
	
	protected boolean hasFunction(String funcName) {
		return cardScope.has(funcName, cardScope) && (cardScope.get(funcName, cardScope) instanceof Function);
	}
	
	protected Object invokeRequiredFunction(String funcName, Object... args) {
		return internalInvokeFunction(funcName, true, args);
	}
	
	protected Object invokeFunction(String funcName, Object... args) {
		return internalInvokeFunction(funcName, false, args);
	}
	
	private Object internalInvokeFunction(String funcName, boolean required, Object... args) throws EvaluatorException {
		if(cardScope == null)
			throw new IllegalStateException("Must load a script before invoking a function");
		
		Object obj = cardScope.get(funcName, cardScope);
		if(!required && (obj == Undefined.instance || obj == Scriptable.NOT_FOUND)) return null; //Simply return null if nonexistent
		if(obj instanceof Function) {
			Function func = (Function) obj;
			return func.call(Initializer.getScriptContext(), cardScope, cardScope, args);
		}
		else throw new IllegalArgumentException(
				"The object \"" + funcName + "\" " + (required ? "does not exist or " : "") + "is not a function");
	}
	
	public abstract Instance newInstance(Session session, Player owner);

	public abstract class Instance {
		@Getter private Session session;
		@Getter private Player owner;
		
		public abstract Card getCard();
		
		protected Instance(Session session, Player owner) {
			this.session = session;
			this.owner = owner;
		}
		
		protected void onInit() {
			invokeFunction("onInit", this);
		}
		
		public String toString() {
			return getDisplayName();
		}
	}
}
